
local wx = require "wx"
local rcr = require "barf.rcr"
local _G = _G
local ipairs,pairs,error,setmetatable,tostring,tonumber
    = ipairs,pairs,error,setmetatable,tostring,tonumber
local strbyte, strsub, strmatch, strrep, strchar, strformat
    = string.byte, string.sub, string.match, string.rep, string.char, string.format
local tconcat = table.concat
local bit = require "bit"
local floor = math.floor


module "barf.editors"


local stats = {
    {short='SG', full='Strength'},
    {short="D",  full='Defence'},
    {short="AG", full='Agility'},
    {short="TH", full='Throw'},
    {short="WN", full='Weapon'},
    {short="K",  full='Kick'},
    {short="P",  full='Punch'},
    {short="WP", full="Will Power"},
    {short="ST", full="Stamina"}
}

local shopitem_meta = {
    __index = {
        coststring = function(self)
            if not self.cents then  return '--';  end
            return strformat('$%0.2f', self.cents/100);
        end;
        statstring = function(self)
            local statbuf = {}
            
            for _,stat in ipairs(stats) do
                if self[stat.short] then
                    statbuf[#statbuf+1] = stat.short .. ': ' .. self[stat.short];
                end
            end
            
            return tconcat(statbuf, ", ")
        end;
    };
    __tostring = function(self)
        if self.cents then
            return strformat('%s (%s) [%s]', self.name, self:coststring(), self:statstring())
        else
            return self.name
        end
    end;
}

local function dec_as_hex(h)
    return tonumber(strformat('%02x',h));
end

local cashvalidator = wx.wxTextValidator(wx.wxFILTER_INCLUDE_CHAR_LIST);
cashvalidator.Includes = {'$','.','0','1','2','3','4','5','6','7','8','9'};

local numbervalidator = wx.wxTextValidator(wx.wxFILTER_INCLUDE_CHAR_LIST);
numbervalidator.Includes = {'0','1','2','3','4','5','6','7','8','9'};

local function parsecost(str)
    local dollars,_,cents = strmatch(str, "^%s*$?(%d+)(%.?(%d*))");
    if (dollars == nil) then
        return nil, "Invalid cost";
    end
    dollars = tonumber(dollars);
    if (dollars > 9999) then
        return nil, "Invalid cost";
    end
    if cents and (#cents > 0) then
        if (#cents > 2) then
            return nil, "Invalid cost";
        end
        cents = tonumber(cents)
    else
        cents = 0
    end
    return cents + (dollars * 100);
end

function shopitem(info, parentWindow)
    -- The ROM image being edited. See barf.nes.readrom()
    local rom = _G.rom;
    
    -- Extract the items to edit
    local items = {};
    for i = 1,info.count do
        local ptr = rom:uint16_le(info.rom, info.bank, info.start + (2*(i-1)));
        ptr = bit.band(ptr, bit.bnot(info.ptr_OR));
        
        local item_name = rom:string(info.rom, info.bank, ptr);
        
        local item = {name = item_name};
        
        if (i > 1) and (i < 126) then
            ptr = ptr + #item_name + 1;
            
            local cents = dec_as_hex( rom:uint8(info.rom, info.bank, ptr) );
            cents = cents + dec_as_hex( rom:uint8(info.rom, info.bank, ptr+1) )*100;
            cents = cents + dec_as_hex( rom:uint8(info.rom, info.bank, ptr+2) )*10000;
            item.cents = cents;
            
            ptr = ptr + 3;
            
            -- Somehow separates items that get "consumed" (like food) when you use them
            -- from items that get "equipped" (like skill books). Investigate further
            item.unknown = rom:uint8(info.rom, info.bank, ptr);
            ptr = ptr + 1;

            -- Action strings
            item.action1 = rom:uint8(info.rom, info.bank, ptr);
            ptr = ptr + 1;
            item.action2 = rom:uint8(info.rom, info.bank, ptr);
            ptr = ptr + 1;
        
            local statflags1 = rom:uint8(info.rom, info.bank, ptr);
            local statflags2 = rom:uint8(info.rom, info.bank, ptr+2);
            ptr = ptr + 2;
            if bit.band(statflags1,0x02)>0 then
                item.SG = rom:uint8(info.rom, info.bank, ptr);
                ptr = ptr + 1;
            end
            if bit.band(statflags1,0x04)>0 then
                item.D = rom:uint8(info.rom, info.bank, ptr);
                ptr = ptr + 1;
            end
            if bit.band(statflags1,0x08)>0 then
                item.AG = rom:uint8(info.rom, info.bank, ptr);
                ptr = ptr + 1;
            end
            if bit.band(statflags1,0x10)>0 then
                item.TH = rom:uint8(info.rom, info.bank, ptr);
                ptr = ptr + 1;
            end
            if bit.band(statflags1,0x20)>0 then
                item.WP = rom:uint8(info.rom, info.bank, ptr);
                ptr = ptr + 1;
            end
            if bit.band(statflags1,0x40)>0 then
                item.K = rom:uint8(info.rom, info.bank, ptr);
                ptr = ptr + 1;
            end
            if bit.band(statflags1,0x80)>0 then
                item.P = rom:uint8(info.rom, info.bank, ptr);
                ptr = ptr + 1;
            end
            if bit.band(statflags1,0x01)>0 then
                item.WP = rom:uint8(info.rom, info.bank, ptr);
                ptr = ptr + 1;
            end
            if bit.band(statflags2,0x80)>0 then
                item.ST = rom:uint8(info.rom, info.bank, ptr);
                ptr = ptr + 1;
            end
        end
        
        items[#items+1] = setmetatable(item, shopitem_meta);
    end
    
    parentWindow.Sizer = wx.wxBoxSizer(wx.wxVERTICAL);
    
    -- Splitter
    local splitter = wx.wxSplitterWindow(
        parentWindow,
        wx.wxID_ANY,
        wx.wxDefaultPosition,
        wx.wxDefaultSize,
        wx.wxSP_3D);
    splitter.SashGravity = 1.0;
    
    -- List box of shop items
    local itemsBox = wx.wxListCtrl(
        splitter,
        wx.wxID_ANY,
        wx.wxDefaultPosition,
        wx.wxDefaultSize,
        wx.wxLC_REPORT);
        
    local col = wx.wxListItem();
    col:SetMask(wx.wxLIST_MASK_TEXT);
    col:SetText("Name");
    itemsBox:InsertColumn(0, col);
    col:SetText("Cost");
    itemsBox:InsertColumn(1, col);
    col:SetText("Stats");
    itemsBox:InsertColumn(2, col);
    
    -- The panel for editing an individual item
    local itemEditPanel = wx.wxPanel(
        splitter,
        wx.wxID_ANY,
        wx.wxDefaultPosition,
        wx.wxDefaultSize);
    
    itemEditPanel.Sizer = wx.wxBoxSizer(wx.wxVERTICAL);
    
    local editName, editCost, updateButton, revertButton;
    
    -- Name field
    do
        local editNameSizer = wx.wxBoxSizer(wx.wxHORIZONTAL)
        local editNameLabel = wx.wxStaticText(
            itemEditPanel,
            wx.wxID_ANY,
            "Name:",
            wx.wxDefaultPosition,
            wx.wxDefaultSize,
            wx.wxALIGN_RIGHT);
        editNameSizer:Add(editNameLabel, 4);
        editName = wx.wxTextCtrl(
            itemEditPanel,
            wx.wxID_ANY,
            "",
            wx.wxDefaultPosition,
            wx.wxDefaultSize,
            wx.wxTE_PROCESS_ENTER,
            rom.romlib.textvalidator);
        editName.MaxLength = 11;
        editNameSizer:Add(editName, 6);
        editNameSizer:Add(0,0,1);
        itemEditPanel.Sizer:Add(editNameSizer, 0, wx.wxEXPAND);
    end
    
    -- Cost field
    do
        local editCostSizer = wx.wxBoxSizer(wx.wxHORIZONTAL)
        local editCostLabel = wx.wxStaticText(
            itemEditPanel,
            wx.wxID_ANY,
            "Cost:",
            wx.wxDefaultPosition,
            wx.wxDefaultSize,
            wx.wxALIGN_RIGHT);
        editCostSizer:Add(editCostLabel, 4);
        editCost = wx.wxTextCtrl(
            itemEditPanel,
            wx.wxID_ANY,
            "",
            wx.wxDefaultPosition,
            wx.wxDefaultSize,
            wx.wxTE_PROCESS_ENTER,
            cashvalidator);
        editCost.MaxLength = 11;
        editCostSizer:Add(editCost, 6);
        editCostSizer:Add(0,0,1);
        itemEditPanel.Sizer:Add(editCostSizer, 0, wx.wxEXPAND);
    end
    
    local editStats = {} do
        for _,stat in ipairs(stats) do
            local editStatSizer = wx.wxBoxSizer(wx.wxHORIZONTAL)
            local editStatLabel = wx.wxStaticText(
                itemEditPanel,
                wx.wxID_ANY,
                stat.full .. ":",
                wx.wxDefaultPosition,
                wx.wxDefaultSize,
                wx.wxALIGN_RIGHT);
            editStatSizer:Add(editStatLabel, 4);
            local editStat = wx.wxTextCtrl(
                itemEditPanel,
                wx.wxID_ANY,
                "",
                wx.wxDefaultPosition,
                wx.wxDefaultSize,
                wx.wxTE_PROCESS_ENTER,
                numbervalidator);
            editStat.MaxLength = 3;
            editStatSizer:Add(editStat, 6);
            editStatSizer:Add(0,0, 1);
            itemEditPanel.Sizer:Add(editStatSizer, 0, wx.wxEXPAND);
            editStats[stat.short] = editStat;
        end
    end
    
    -- Action buttons (Update)
    do
        local actionSizer = wx.wxBoxSizer(wx.wxHORIZONTAL);
        
        actionSizer:Add(0,0,4);
        
        updateButton = wx.wxButton(
            itemEditPanel,
            wx.wxID_ANY,
            "Update",
            wx.wxDefaultPosition,
            wx.wxDefaultSize,
            wx.wxBU_EXACTFIT);
        actionSizer:Add(updateButton, 3, wx.wxCENTER);
        
        revertButton = wx.wxButton(
            itemEditPanel,
            wx.wxID_ANY,
            "Revert",
            wx.wxDefaultPosition,
            wx.wxDefaultSize,
            wx.wxBU_EXACTFIT);
        actionSizer:Add(revertButton, 3, wx.wxCENTER);
        
        actionSizer:Add(0,0,1);
        
        itemEditPanel.Sizer:Add(actionSizer, 0, wx.wxEXPAND);
    end
   
    splitter.MinimumPaneSize = itemEditPanel.Sizer.MinSize.Width;
    
    local function selectItem(item)
        editName.Value = item.name;
        editName:Enable(true);
        if item.cents then
            editCost.Value = item:coststring();
            editCost:Enable(true);
            for _,stat in pairs(stats) do
                stat = stat.short;
                local edit = editStats[stat];
                edit.Value = tostring(item[stat] or '');
                edit:Enable(true);
            end
        else
            editCost.Value = "";
            editCost:Enable(false);
            for _,stat in pairs(stats) do
                stat = stat.short;
                local edit = editStats[stat];
                edit.Value = '';
                edit:Enable(false);
            end
        end
    end
    
    local function deselectItem()
        editName.Value = '';
        editName:Enable(false);
        editCost.Value = '';
        editCost:Enable(false);
        updateButton:Enable(false);
        revertButton:Enable(false);
        for _,stat in pairs(stats) do
            local edit = editStats[stat.short];
            edit.Value = '';
            edit:Enable(false);
        end
    end
    
    local function writeback()
        local ptr = info.start + (info.count * 2);
        for i,item in ipairs(items) do
            local ptrptr = info.start + ((i-1)*2);
            rom:uint16_le(info.rom, info.bank, ptrptr, info.ptr_OR + ptr);
            ptr = rom:string(info.rom, info.bank, ptr, item.name);
            if item.cents then
                local cents = item.cents % 100;
                local dollars = floor(item.cents / 100) % 100;
                local hundreds_of_dollars = floor(item.cents / 10000) % 100;
                ptr = rom:uint8(info.rom, info.bank, ptr, tonumber(cents,16));
                ptr = rom:uint8(info.rom, info.bank, ptr, tonumber(dollars,16));
                ptr = rom:uint8(info.rom, info.bank, ptr, tonumber(hundreds_of_dollars,16));
                ptr = rom:uint8(info.rom, info.bank, ptr, item.unknown);
                ptr = rom:uint8(info.rom, info.bank, ptr, item.action1);
                ptr = rom:uint8(info.rom, info.bank, ptr, item.action2);
                local statflags1,statflags2 = 0,0
                if item.SG then  statflags1 = bit.bor(statflags1,0x02);  end
                if item.D then   statflags1 = bit.bor(statflags1,0x04);  end
                if item.AG then  statflags1 = bit.bor(statflags1,0x08);  end
                if item.TH then  statflags1 = bit.bor(statflags1,0x10);  end
                if item.WP then  statflags1 = bit.bor(statflags1,0x20);  end
                if item.K then   statflags1 = bit.bor(statflags1,0x40);  end
                if item.P then   statflags1 = bit.bor(statflags1,0x80);  end
                if item.WP then  statflags1 = bit.bor(statflags1,0x01);  end
                if item.ST then  statflags2 = bit.bor(statflags2,0x80);  end
                ptr = rom:uint8(info.rom, info.bank, ptr, statflags1);
                ptr = rom:uint8(info.rom, info.bank, ptr, statflags2);
                if item.SG then  ptr = rom:uint8(info.rom, info.bank, ptr, item.SG);  end
                if item.D  then  ptr = rom:uint8(info.rom, info.bank, ptr, item.D);   end
                if item.AG then  ptr = rom:uint8(info.rom, info.bank, ptr, item.AG);  end
                if item.TH then  ptr = rom:uint8(info.rom, info.bank, ptr, item.TH);  end
                if item.WP then  ptr = rom:uint8(info.rom, info.bank, ptr, item.WP);  end
                if item.K  then  ptr = rom:uint8(info.rom, info.bank, ptr, item.K);   end
                if item.P  then  ptr = rom:uint8(info.rom, info.bank, ptr, item.P);   end
                if item.WP then  ptr = rom:uint8(info.rom, info.bank, ptr, item.WP);  end
                if item.ST then  ptr = rom:uint8(info.rom, info.bank, ptr, item.ST);  end
                --]]
            end
        end
    end
    
    itemEditPanel:Layout();
    
    splitter:SplitVertically(itemsBox,itemEditPanel);
    
    parentWindow.Sizer:Add(splitter, 1, wx.wxEXPAND + wx.wxALL, 0);
    
    parentWindow:Layout();
    
    deselectItem();
    
    for i,item in ipairs(items) do
        local row = itemsBox:InsertItem(itemsBox:GetItemCount(), item.name);
        itemsBox:SetItem(row,0,item.name);
        if item.cents then
            itemsBox:SetItem(row,1,item:coststring());
            itemsBox:SetItem(row,2,item:statstring());
        end
        item.index = row;
    end
    
    local selectedIndex;

    local function clearChanges()
        updateButton:Enable(false);
        revertButton:Enable(false);
    end
    
    local function clickUpdateButton()
        updateButton:ProcessEvent(
            wx.wxCommandEvent(wx.wxEVT_COMMAND_BUTTON_CLICKED, updateButton.Id)
        );
    end
    
    local function enableWhenModified(event)
        if event.EventObject:DynamicCast("wxTextCtrl"):IsModified() then
            updateButton:Enable(true);
            revertButton:Enable(true);
        end
    end
    
    local ESC = wx.WXK_ESCAPE;
    local function revertOnEsc(event)
        if (event.KeyCode == ESC) then
            revertButton:ProcessEvent(
                wx.wxCommandEvent(wx.wxEVT_COMMAND_BUTTON_CLICKED, revertButton.Id)
            );
            return;
        end
        event:Skip();
    end
    
    editName:Connect(wx.wxID_ANY, wx.wxEVT_KEY_DOWN, revertOnEsc);
    editCost:Connect(wx.wxID_ANY, wx.wxEVT_KEY_DOWN, revertOnEsc);
    for _,edit in pairs(editStats) do
        edit:Connect(wx.wxID_ANY, wx.wxEVT_KEY_DOWN, revertOnEsc);
    end
    
    editName:Connect(wx.wxID_ANY, wx.wxEVT_COMMAND_TEXT_ENTER, clickUpdateButton);
    editCost:Connect(wx.wxID_ANY, wx.wxEVT_COMMAND_TEXT_ENTER, clickUpdateButton);
    for _,edit in pairs(editStats) do
        edit:Connect(wx.wxID_ANY, wx.wxEVT_COMMAND_TEXT_ENTER, clickUpdateButton);
    end

    editName:Connect(wx.wxID_ANY, wx.wxEVT_COMMAND_TEXT_UPDATED, enableWhenModified);
    editCost:Connect(wx.wxID_ANY, wx.wxEVT_COMMAND_TEXT_UPDATED, enableWhenModified);
    for _,edit in pairs(editStats) do
        edit:Connect(wx.wxID_ANY, wx.wxEVT_COMMAND_TEXT_UPDATED, enableWhenModified);
    end
        
    itemsBox:Connect(wx.wxEVT_COMMAND_LIST_ITEM_SELECTED,
        function(event)
            selectedIndex = event.Index + 1;
            local item = items[selectedIndex];
            selectItem(item);
        end);
    
    itemsBox:Connect(wx.wxEVT_COMMAND_LIST_ITEM_ACTIVATED,
        function()
            editName:SetFocus();
        end);
    
    revertButton:Connect(revertButton.Id, wx.wxEVT_COMMAND_BUTTON_CLICKED,
        function()
            if selectedIndex then
                selectItem(items[selectedIndex]);
            end
            clearChanges();
            itemsBox:SetFocus();
        end);
        
    updateButton:Connect(updateButton.Id, wx.wxEVT_COMMAND_BUTTON_CLICKED,
        function()
            if selectedIndex then
                local item = items[selectedIndex];
                
                -- Validation
                local new_cents
                local new_stats
                if item.cents then
                    local message;
                    new_cents, message = parsecost(editCost.Value);
                    if not new_cents then
                        wx.wxMessageBox(message, "Invalid Cost Value", wx.wxOK + wx.wxICON_ERROR);
                        editCost:SetFocus();
                        return;
                    end
                    new_stats = {}
                    for _,stat in ipairs(stats) do
                        stat = stat.short;
                        local edit = editStats[stat];
                        local new_value = tonumber(edit.Value) or false;
                        if new_value and (new_value > 255) then
                            wx.wxMessageBox("Maximum stat value is 255",
                                "Invalid Stat Value", wx.wxOK + wx.wxICON_ERROR);
                            edit:SetFocus();
                            return;
                        end
                        new_stats[stat] = new_value;
                    end
                end
                
                -- Set values
                local new_name = editName.Value;
                item.name = new_name;
                itemsBox:SetItem(selectedIndex-1,0,new_name);
                
                if new_cents then
                    item.cents = new_cents;
                    local new_cost = item:coststring();
                    editCost.Value = new_cost;
                    itemsBox:SetItem(selectedIndex-1,1,new_cost);
                    for stat,value in pairs(new_stats) do
                        item[stat] = value;
                    end
                    itemsBox:SetItem(selectedIndex-1,2,item:statstring());
                end
                
                writeback();
            end
            clearChanges();
            itemsBox:SetFocus();
        end);
end

return shopitem;
